/**
 *
 * @author Chris
 */
import java.io.*;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.net.SocketTimeoutException;

public class ServiceProviderServer implements Runnable {

	private Hashtable<String, String> config;

	private Hashtable<String, Hashtable<String, String>> db;

	private boolean serverRunning;

	private int clientCount;

	//Locks for the serverRunning, clientCount and db members
	private Lock dbLock;

	private Lock serverRunningLock = new ReentrantLock();

	private Lock clientCountLock = new ReentrantLock();

	private Lock socketLock = new ReentrantLock();

	private Condition clientCountZero = this.clientCountLock.newCondition();

	private ServerSocket serv;

	public ServiceProviderServer(Hashtable<String, Hashtable<String, String>> db, Hashtable<String, String> config, Lock dbLock) {

		this.config = config;

		this.db = db;

		this.dbLock = dbLock;

	}

	public void run() {

		serverRunningLock.lock();
		serverRunning = true;
		serverRunningLock.unlock();

		//write port ot console
		System.out.println("Started server");

		clientCountLock.lock();
		clientCount = 0;
		clientCountLock.unlock();
		System.out.println("listening on port " + config.get("PORT"));

		while (serverRunning) {
			try {

				socketLock.lock();
				if (serv == null || serv.isClosed())
					serv = new ServerSocket(Integer.parseInt(config.get("PORT")));
					socketLock.unlock();
					serv.setSoTimeout(1000);
					
				do {

					Socket clientConnection = null;
					if (Thread.interrupted())
						serverRunning = false;

					if (!serverRunning)
						break;

					clientConnection = serv.accept();
					System.out.println("incoming client...");


					//Status update
					System.out.println("Accepted client connection");

					//Start client thread
					Thread clientThread = new Thread(new clienthandler(
							clientCount, clientConnection));

					clientThread.start();

					clientCountLock.lock();
					clientCount++;
					clientCountLock.unlock();

				} while (serverRunning);

				serv.close();

			} catch (SocketTimeoutException e) { 
			} catch (InterruptedIOException e) {
				serverRunning = false;
			} catch (SocketException e) {
				if (clientCount == 0 || serverRunning) {
					System.out.println(msg.E4 + "\n\n");
					e.printStackTrace();
				}
			} catch (IOException e) {

				if (e instanceof BindException) {
					System.out.println(msg.E1 + " Exiting");

					serverRunningLock.lock();
					serverRunning = false;
					serverRunningLock.unlock();

				} else {
					System.out.println(msg.E4 + "\n");
					e.printStackTrace();
				}
			} finally {

				//Test the locks and lock them if possible
				serverRunningLock.tryLock();
				dbLock.tryLock();
				clientCountLock.tryLock();

				//Now make sure they are all released before resuming
				serverRunningLock.unlock();
				dbLock.unlock();
				clientCountLock.unlock();
			}
		}

		try {
			clientCountLock.lock();

			if (this.clientCount != 0)
				clientCountZero.await();
			else
				clientCountLock.unlock();

			System.out.println("Saving datastore");
			DbStorage.save(db, config.get("DATABASE"));
			System.out.println("Shutting down server");
		} catch (InterruptedException e) {
		}
		System.exit(0);
	}

	//this class represents the client handler for incoming connections
	public class clienthandler implements Runnable {

		private Socket clientConnection;

		private boolean running;

		private boolean ishandShake;

		private int id;

		private boolean hasdblock = false;

		public clienthandler(int id, Socket clientConnection) {
			this.id = id;
			this.clientConnection = clientConnection;

		}

		//this is the central method for the thread
		public void run() {
			try {

				//Get stream from client
				BufferedReader sr = new BufferedReader(new InputStreamReader(
						clientConnection.getInputStream()));

				//Get stream to write to client
				BufferedWriter sw = new BufferedWriter(new OutputStreamWriter(
						clientConnection.getOutputStream()));

				this.running = true;

				this.ishandShake = true;

				while (running) {

					//read input from client
					String indata = sr.readLine();

					System.out.println("CLIENT: " + indata);

					//parse the input and get the reply
					String[] reply = process(indata);

					System.out.print("SERVER" + this.id + ": ");

					if (reply == null) {
						continue;
					}

					for (String s : reply) {
						System.out.println(s);
						sw.write(s + "\n");
						sw.flush();
					}
				}

				this.clientConnection.close();

			} catch (IOException e) {
				System.out.println(msg.E4);
				decCount();
				if (hasdblock) {
					dbLock.unlock();
				}
			}
		}

		//this method provides synchronized access to the client count
		private void decCount() {
			clientCountLock.lock();
			clientCount--;
			if (clientCount == 0) {
				clientCountZero.signal();
			}
			clientCountLock.unlock();
		}

		//this method is the central handler method for the client
		private String[] process(String s) {

			String[] statements = s.split(" ");

			String[] reply = null;

			int MESSAGE = 0;
			try {
				//Process the statement
				if (statements[MESSAGE].equalsIgnoreCase(msg.Shutdown)) {
					//lock the serverrunning variable
					serverRunningLock.lock();
					serverRunning = false;
					serverRunningLock.unlock();

					socketLock.lock();
					serv.close();
					socketLock.unlock();

					reply = new String[] { msg.Ok };
				} else if (!this.ishandShake
						&& statements[MESSAGE].equalsIgnoreCase(msg.Hello)) {
					reply = new String[] { msg.E3 };
				} else if (this.ishandShake
						&& statements[MESSAGE].equalsIgnoreCase(msg.Hello)) {

					reply = new String[] { msg.Hi };

					this.ishandShake = false;

				} else if (this.ishandShake
						&& !statements[MESSAGE].equalsIgnoreCase(msg.Hello)) {

					reply = new String[] { msg.E2 };
					this.running = false;
					//upate client cound in containing
					decCount();

				} else if (statements[MESSAGE].equalsIgnoreCase(msg.Register)) {
					//register a new triple
					reply = register(statements);
				} else if (statements[MESSAGE].equalsIgnoreCase(msg.Query)) {
					//remove the given triple
					reply = serviceProviderQuery(statements);
				} else if (statements[MESSAGE].equalsIgnoreCase(msg.Remove)) {
					//Remove the given triple from the db
					reply = remove(statements, s);
				} else if (statements[MESSAGE].equalsIgnoreCase(msg.Bye)) {
					//this.running = false;
					this.running = false;
					reply = new String[] { msg.Later };
					//Update the client count in the containing class
					decCount();
				} else {
					reply = new String[] { msg.E5 };
				}
			} catch (IOException e) {
			}

			return reply;
		}

		private String[] serviceProviderQuery(String[] statements) {

			String[] reply = null;
			int ID = 1;
			int VALUE1 = 2;
			int VALUE2 = 3;
			if (statements.length != msg.QueryFieldLength) {
				reply = new String[] { msg.E3 };
			} else {

				//Perform query given the criteria
				dbLock.lock();
				hasdblock = true;

				//retrieve the triple set
				Hashtable<String, String> content = db.get(statements[VALUE1]);

				String result = "";
				int resultCount = 0;
				//Handle case where multiple results are returned
				if (content != null && statements[VALUE2].equals("*")) {
					reply = new String[content.size() + 1];
					//Copy results into result set
					for (String k : content.keySet()) {
						reply[resultCount + 1] = msg.Response(statements[ID],
								resultCount + 1, content.get(k));
						resultCount++;
					}
				} else if (content != null && !statements[VALUE2].equals("*")) {
					//retrieve specific result
					result = content.get(statements[VALUE2]);
					if (result != null) {
						resultCount++;
						reply = new String[] { "",
								msg.Response(statements[ID], 1, result) };
					}
				}
				if (resultCount == 0) {
					reply = new String[1];
				}

				dbLock.unlock();
				hasdblock = false;

				//Set the first message to be the query-reply
				reply[0] = msg.QueryReply(statements[ID], resultCount);
			}
			return reply;
		}

		private String[] remove(String[] statements, String s) {

			//Check format of message, return error if wrong
			if (statements.length != msg.RemoveFieldLength) {
				return new String[] { msg.E3 };
			}

			int VALUE1 = 1;
			int VALUE2 = 2;
			int AFTER_REMOVE = 7;

			//lock db and remove record if its found
			dbLock.lock();
			hasdblock = true;
			Hashtable<String, String> content = db.get(statements[VALUE1]);

			//check if such a record set exists in db
			if (content == null) {
				dbLock.unlock();
				return new String[] { msg.StatusF };
			}

			String result = content.get(statements[VALUE2]);

			//Check if such a record exists in the db
			if (result == null) {
				dbLock.unlock();
				return new String[] { msg.StatusF };
			}

			//Finally, if we've passed all other tests, then check if the third field matches, if not return false
			if (!result.equalsIgnoreCase(s.substring(AFTER_REMOVE))) {
				dbLock.unlock();
				return new String[] { msg.StatusF };
			}

			//Otherwise, remove the triple and return true!
			content.remove(statements[VALUE2]);

			db.put(statements[VALUE1], content);

			//Release lock on db
			dbLock.unlock();
			hasdblock = false;

			return new String[] { msg.StatusT };

		}

		private String[] register(String[] statements) {

			String[] reply = null;
			int VALUE1 = 1;
			int VALUE2 = 2;
			int VALUE3 = 3;

			if (statements.length != msg.RegisterFieldLength) {
				reply = new String[] { msg.E3 };
			} else {
				//lock db and add statement to it
				dbLock.lock();
				hasdblock = true;
				Hashtable<String, String> content = db.get(statements[VALUE1]);
				if (content == null) {
					content = new Hashtable<String, String>();
				}

				//Insert triple into hashtable with newline so that a future query which will have a newline, can find this
				content.put(statements[VALUE2], statements[VALUE1] + " "
						+ statements[VALUE2] + " " + statements[VALUE3]);
				db.put(statements[VALUE1], content);

				//save db to disk
				DbStorage.save(db, config.get("DATABASE"));
				
				//release lock on db
				dbLock.unlock();
				hasdblock = false;
				reply = new String[] { msg.StatusT };
			}

			return reply;
		}
	}
}
